fix(cli): os explain query teaches where / orderBy, and the catalog sweep asserts key retention - #17263
Conversation
…rt key retention The `query` catalog entry taught `filters` and `sort`. `BaseQuerySchema` is a plain `z.object`, so both were dropped silently: the example parsed clean and came back with no filter and no ordering, and the #14811 sweep was green on it because `safeParse(...).success` stays true through a silent strip. Both faces corrected to the schema's own spellings — `where` (one condition tree, not a `Filter[]`) and `orderBy` (sort nodes whose direction key is `order`, never `direction`). The sweep grows a key-retention assertion: an example must parse AND survive the parse with every declared key intact. Green across all nine bound entries today — eight refuse unknown keys outright and `query` was the only open top level — so it lands as a ratchet against a bound entry resolving to an open top level again, not as a patch over a large hole. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
…orrection Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a9bc6955288edaa28b4a427e8fbace8130642cd5 && git checkout a9bc6955288edaa28b4a427e8fbace8130642cd5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 470746ae4511db2f7c754d8aa6c247c3ce2120c4 582bf0dd5fddfdcb8afb77111db0b87b38c7f26a && git checkout -B drift-repro 470746ae4511db2f7c754d8aa6c247c3ce2120c4 && git merge --no-ff 582bf0dd5fddfdcb8afb77111db0b87b38c7f26a
node scripts/docs-audit/affected-docs.mjs --json 470746ae4511db2f7c754d8aa6c247c3ce2120c4
|
ACCEPT — armed for the queueDispatching seat ( Pre-checks, each read rather than taken from the delivery report:
⭐ What makes the new assertion worth its line. The pre-existing sweep asserted It lands as a ratchet, not a patch over a hole: all nine bound entries were read back to spec, eight refuse an unknown key outright and Residue, filed rather than carried: the retention assertion reads the example face only, and the optional/required table rows carry key names too — the same authoring trap, unguarded, and half of this card's own defect lived there. Filed as #17266 with both obstacles the delivery measured (a table row is prose, not a key; one entry of nine is not introspectable through its wrapper) and the fences it inherits.
Generated by Claude Code |
Fixes #16925
Clause-②: no
os explain querydocumented two keysQuerySchemadoes not have. Both faces ofthe entry now read the schema's own spellings, and the #14811 catalog sweep grows
the assertion that can actually see this failure mode.
The defect, measured at
origin/mainBaseQuerySchema(packages/spec/src/data/query.zod.ts) is a plainz.object,so an unknown top-level key is dropped, not refused. The shipped example was
therefore green through the sweep and empty in the result:
An author who copied it got a query that parsed clean and ran with no filter and
no ordering — with
limit: 50sitting next to it, a different set of rows underan ordinary success, and nothing in the output saying so.
Half 1 — the entry, both faces
The real shapes were read off
BaseQuerySchemadirectly, not inferred from theold
Filter[]/SortConfig[]type names:wheretakesFilterConditionSchema— one condition tree, not anarray. A field-keyed entry is a condition on that field (a bare value is
implicit equality, an object is a map of
$operators), and$and/$or/$notcombine conditions.orderBytakesSortNodeSchema[], whose direction key is spelledorder.directionis not a synonym —SortNodeSchemais strict andrefuses it by name, because when it was merely stripped,
orderfell back toits
ascdefault and a descending request came back ascending.Both the example and the two optional-table rows were corrected. Verified by
parse, on the corrected literal:
Half 2 — the sweep grows a key-retention assertion
The #14811 sweep asserts
safeParse(...).success === true, and a silent stripkeeps that true — the green over this entry was false by construction. The
new assertion is the one that can see it: an example must parse and come back
with every key it declares.
It lands as a ratchet, not as a patch over a large hole. All nine
BOUNDentries were re-measured against spec for this PR by feeding each schema an
unknown top-level key:
Class size is 1 of 9, and the direction of travel is closing — the open top
level is declared deliberate and already owned (
query.zod.ts: 「Deliberately NOT taken here:BaseQuerySchema's own top level stays non-strict. That is#4001's to schedule.」). So the assertion is green across the whole catalog
the day it lands; what it defends is the day a bound entry resolves to an open
top level again.
It covers the EXAMPLE face only, deliberately.
evaluate(key)readscatalog[key].exampleand has never read theoptional/requiredtables —and this card's two wrong spellings lived on both faces. Covering the table
face is not a stricter version of this assertion but a different one, and the
measurement says why: a table row is prose, not a key. Measured across the nine
entries, rows that are not a key of their schema:
So a table-face assertion needs a way to tell a key name from a description, and
a second technique for the one entry whose shape it cannot read. That is a design
decision rather than a mechanical extension, so it is declared as a gap in the
test's own header and raised as an open question rather than half-built here.
Ablation — predictions written before any leg ran
Recorded in full before the first leg. Restore the pre-fix
filters/sortspellings in the example and re-run; the contrast is the whole card.
expected [ 'filters', 'sort' ] to deeply equal []safeParseassertion stays GREEN✓ os explain query — example parses as QuerySchemaTests 1 failed, 39 passed of 40, wrapper exit 1Tests 40 passed (40), wrapper exit 0git diff HEADemptyNo prediction came out wrong. The mutation was proven on disk before the run —
occurrence counts moved (
whereanchor 1 to 0,filtersanchor 0 to 1) and theblob hash moved off HEAD's
09ab4c72…to6f8c85f5…; the restore wasgit checkout HEAD -- ABSPATH(never bare), proven by blob equality,git diff HEADat 0 bytes and a cleangit status --porcelain, all under atrap ... EXIT INT TERMwith absolute paths. No build leg is involved: the testimports
../src/commands/explainby relative path, so the mutation reaches therun from source.
Verification
pnpm --filter @objectstack/cli exec vitest run --project unit— 190 files,2644 passed. The
integrationtier is declared to CI: this diff touches nospawn entry, no
bin/, and notest/helpers/serve-process.ts.pnpm --filter '@objectstack/cli^...' build— dependency closure green;pnpm --filter @objectstack/cli buildgreen (twopublished-subpathpins readthe package's own
dist/and exit loudly without it — a prerequisite, not afinding).
pnpm --filter @objectstack/cli typecheck— reported below.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackand reconciled with
--ran: 61 commands, 58 exit 0. Every exit code wascaptured by redirect-then-
$?, never through a pipe.Docs-drift round
Run on the final head.
scripts/docs-audit/affected-docs.mjsnamed 15 candidatepages; none carries the wrong spelling at query level — the hand-written docs were
already correct and
os explainwas the outlier (content/docs/data-modeling/queries.mdx:22-23is the corrected shape verbatim). The four pages a prose-class sweep turns up for
filters:/sort:are all a different surface (a nav item'sfilters, asingle view's
sort), andcontent/docs/ui/react-pages.mdxalready documents thesilent-drop behaviour this change makes more true, correctly, with no edit owed.
Positive control for the sweep pattern: 2 hits in
content/docs/ui/views.mdxand 2in
explain.tsatorigin/main. Nothing underdocs/(which the tool never walks)carries the shape either. No page edited; ⛔ nothing under
content/docs/releases/**was touched.
Acceptance notes / 验收备注
Noted, not filed — none of these is a reproducible defect, a contract violation, or
an authoring trap:
view's optional table does not hold on today'stree.
view'soptionalrows arename/label/objectand itsrequiredrow is the slot row — no
filters/sortrows. The two spellings appear inview's prose (a line comment and the container description), where theycorrectly describe a single view's keys one level down. The fence was honoured
regardless:
viewis untouched, and its example remains [finding]os explain view's example teaches a flat view literal —ViewSchemais a CONTAINER (list/form/listViews/formViews) #15171's.filterswhere the entrydescription spells
filter. An observation about a comment on another card'sentry — not filed, not edited.
query's optional table lists six ofQuerySchema's seventeen keys —search,searchFields,top,aggregations,groupBy,havingandexpandare absent.Missing members are an omission, not an error, and out of this card's scope.
sweep, none of them a finding; re-run after a full build, reported in the dev report.
Fences honoured
packages/specuntouched — the open top level stays 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001's. The diff ispackages/cli/src/commands/explain.ts,packages/cli/test/commands.test.tsandone changeset.
viewnot edited. ⛔ No accept set relaxed, no public face widened: the schemadid not move, only the catalog entry that misdescribed it.
Generated by Claude Code